package com.avcompris.util.junit;

import static com.avcompris.util.ExceptionUtils.nonNullArgument;
import static org.apache.commons.lang3.CharEncoding.UTF_8;
import static org.apache.commons.lang3.StringUtils.substringAfter;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.List;

import org.apache.commons.io.FileUtils;
import org.junit.Test;

import com.avcompris.common.annotation.Nullable;
import com.avcompris.util.AbstractUtils;

public abstract class JUnitUtils extends AbstractUtils {

	/**
	 * find the name of the test method (annotated with <tt>@Test</tt>)
	 * currently executing, or <tt>null</tt>
	 */
	public static TestMethodAndClassNames getCurrentTestMethodAndClassNames() {

		return getCurrentTestMethodAndClassNames(".tmp");
	}

	private static TestMethodAndClassNames getCurrentTestMethodAndClassNames(
			final String extension) {

		nonNullArgument(extension, "extension");

		final ClassLoader classLoader = Thread.currentThread()
				.getContextClassLoader();

		for (final StackTraceElement ste : new Throwable().getStackTrace()) {

			final String className = ste.getClassName();
			final String methodName = ste.getMethodName();

			if (className == null || methodName == null) {
				continue;
			}

			final Class<?> c;

			try {

				c = Class.forName(className, false, classLoader);

			} catch (final ClassNotFoundException e) {
				continue;
			}

			final Method m;

			try {

				m = c.getMethod(methodName);

			} catch (final NoSuchMethodException e) {
				continue;
			}

			if (m.getAnnotation(Test.class) != null) {

				final File outFile = new File("target", className + "."
						+ methodName + "_" + System.currentTimeMillis()
						+ extension);

				return new TestMethodAndClassNames(className, methodName,
						outFile);
			}
		}

		return TestMethodAndClassNames.NULL_NAMES;
	}

	public static class TestMethodAndClassNames {

		@Nullable
		public final String className;

		@Nullable
		public final String methodName;

		public final File outFile;

		private TestMethodAndClassNames(@Nullable final String className,
				@Nullable final String methodName, final File outFile) {

			this.className = className;
			this.methodName = methodName;

			this.outFile = nonNullArgument(outFile, "outFile");
		}

		public static final TestMethodAndClassNames NULL_NAMES = new TestMethodAndClassNames(
				null, null, new File("target/"));
	}

	/**
	 * find the name of the test method (annotated with <tt>@Test</tt>)
	 * currently executing, or <tt>null</tt>
	 */
	@Nullable
	public static String getCurrentTestMethodName() {

		return getCurrentTestMethodAndClassNames().methodName;
	}

	/**
	 * load a plain text fragment from comments in or just below the current
	 * test method. This is useful for instance to display in the cleanest way
	 * the XML document or YAML stream to parse and the API usage in the same
	 * Java file. Note: Groovy would just not have this issue.
	 * <p/>
	 * TODO: externalize this method, use AST in it.
	 */
	public static File createTmpFileFromCommentsAroundThisMethod()
			throws IOException {

		return createTmpFileFromCommentsAroundThisMethod(null);
	}

	/**
	 * load a plain text fragment from comments in or just below the current
	 * test method. This is useful for instance to display in the cleanest way
	 * the XML document or YAML stream to parse and the API usage in the same
	 * Java file. Note: Groovy would just not have this issue.
	 * <p/>
	 * TODO: externalize this method, use AST in it.
	 * 
	 * @see #createTmpFileFromCommentsAroundThisMethod()
	 * @param index
	 *            (start at 0) the index of the comments to extract within the
	 *            method.
	 */
	public static File createTmpFileFromCommentsAroundThisMethod(final int index)
			throws IOException {

		return createTmpFileFromCommentsAroundThisMethod(null, index);
	}

	/**
	 * @see #createTmpFileFromCommentsAroundThisMethod()
	 * @param thisMethodName
	 *            the name of the test method in which, or around which, search
	 *            for text content in Java comments. If this parameter is
	 *            <tt>null</tt>, rely on the JUnit test method currently
	 *            executing.
	 */
	public static File createTmpFileFromCommentsAroundThisMethod(
			@Nullable final String thisMethodName) throws IOException {

		return createTmpFileFromCommentsAroundThisMethod(thisMethodName, 0);
	}

	/**
	 * @see #createTmpFileFromCommentsAroundThisMethod()
	 * @param thisMethodName
	 *            the name of the test method in which, or around which, search
	 *            for text content in Java comments. If this parameter is
	 *            <tt>null</tt>, rely on the JUnit test method currently
	 *            executing.
	 * @param index
	 *            (start at 0) the index of the comments to extract within the
	 *            method.
	 */
	public static File createTmpFileFromCommentsAroundThisMethod(
			@Nullable final String thisMethodName, final int index)
			throws IOException {

		final TestMethodAndClassNames t = getCurrentTestMethodAndClassNames();

		final String testClassName = t.className;
		final String testMethodName = (thisMethodName == null) ? t.methodName
				: thisMethodName;

		if (testClassName == null || testMethodName == null) {
			throw new IllegalStateException(
					"Cannot find current test class or test method.");
		}

		final File outFile = t.outFile;

		final File testClassFile = new File("src/test/java",
				testClassName.replace('.', '/') + ".java");

		if (!testClassFile.exists()) {
			throw new FileNotFoundException(
					"Java file for test class not found: "
							+ testClassFile.getAbsolutePath());
		}

		final List<String> lines = FileUtils.readLines(testClassFile, UTF_8);

		final OutputStream os = new FileOutputStream(outFile);
		try {

			final PrintWriter pw = new PrintWriter(new OutputStreamWriter(os,
					UTF_8));
			try {

				boolean methodStarted = false;
				boolean commentsStarted = false;
				int offset = 0;

				for (final String line : lines) {

					if (line.contains("@Test")) {
						if (methodStarted) {
							break;
						}
					}

					if (line.contains(testMethodName + "()")) {
						methodStarted = true;
					}

					if (!methodStarted) {
						continue;
					}

					if (line.trim().startsWith("//")) {

						commentsStarted = true;

						if (offset == index) {

							pw.println(substringAfter(line, "//"));
						}

					} else if (commentsStarted) {

						if (offset >= index) {

							break;
						}

						commentsStarted = false;
						
						++offset;
					}
				}

				pw.flush();

			} finally {
				pw.close();
			}

		} finally {
			os.close();
		}

		return outFile;
	}

	/**
	 * create a File object in the "<tt>target/<tt>" directory with name 
	 * of current test class and method.
	 */
	public static File newTmpFileNamedAfterCurrentTest(final String extension) {

		return getCurrentTestMethodAndClassNames(extension).outFile;
	}
}
